Skip to content

feat: deterministic close on the wrappers that own native state - #519

Open
milyin wants to merge 1 commit into
zenoh-flat-transitionfrom
fix/deterministic-close
Open

feat: deterministic close on the wrappers that own native state#519
milyin wants to merge 1 commit into
zenoh-flat-transitionfrom
fix/deterministic-close

Conversation

@milyin

@milyin milyin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Implements the lifecycle policy from #518: a public wrapper exposes close() when it directly owns native state whose lifetime can escape the call.

ZBytes — discardable and closeable

ZBytes now implements AutoCloseable and exposes discard(). It needs no new state field — the two existing fields already encode every state, and eager == null && handle == null is "discarded":

state discard() later toBytes()
value (from(...)) no-op bytes
received, unread frees the native buffer ZError
received, already materialized no-op bytes
discarded no-op (idempotent) ZError

discard() is @Synchronized on the same monitor as the lazy materialization, so read-vs-discard has two deterministic outcomes and never a use-after-free. The getter's handle!! became handle ?: throw ZError(...), so a read after discard is a clear closed-state error rather than an NPE — that covers toString/equals/hashCode/toZZBytes too, which all route through bytes. No Cleaner is added: the receive hot path is unchanged, and IntoZBytes is untouched.

Config — AutoCloseable

close() delegates to the generated handle's already-idempotent close(), keeping the Cleaner as the forgotten-close fallback. Zenoh.open uses zConfig.newClone(...) rather than consuming the config, so closing stays the user's decision; afterwards getJson/insertJson5/opening a session fail through the generated closed-handle guard as ZError. The class KDoc said the config was "consumed by open" — corrected.

SessionDeclaration : AutoCloseable

close() = undeclare() as a default. The six implementations (Publisher, Subscriber, Queryable, Querier, KeyExpr, LivelinessToken) already had exactly that override and keep it — this repo builds with Kotlin 1.9, where interface bodies compile to DefaultImpls, so leaving the per-class overrides in place is what keeps Java binary compatibility unconditional. The default is there for external/polymorphic use, as the issue asks.

Out of scope per the issue: Sample, Reply, Query, Encoding, and the Into* facades.

Tests

New ZBytesLifecycleTest.kt builds handle-backed instances directly (no session — the wrapper's state machine is what is under test) and covers: discard before materialization, discard after, repeated close()/discard(), a user-created ZBytes, and 500 rounds of concurrent read-vs-close asserting each read either returns the payload or throws ZError — nothing else. Plus one assertion that SessionDeclaration is AutoCloseable. ConfigTest gains a double-close/use-after-close test.

./gradlew :zenoh-java:test -PuseLocalFlatJni=true — 119 tests, all green (-PuseLocalFlatJni is needed because org.eclipse.zenoh:zenoh-flat-jni:1.9.0 is not on Maven yet).

Closes #518

Three public wrappers directly own native memory whose lifetime escapes the
call, but only some of them let a caller release it:

- ZBytes holds a native buffer until first materialization, and is the one
  class here with no GC backstop (a per-message Cleaner costs -23% throughput),
  so an unread payload was unreleasable. It is now AutoCloseable with an
  explicit discard(): idempotent, taking the same monitor as the lazy
  materialization, so a concurrent read either wins (bytes stay readable) or
  loses (a later read fails with ZError instead of an NPE on handle!!).
  Discarding an already-materialized or user-created ZBytes is a no-op: the
  bytes stay readable. No Cleaner is added.
- Config owns a Cleaner-backed native config; close() releases it eagerly and
  keeps the Cleaner as the forgotten-close fallback. Opening a session copies
  the config rather than taking it, so closing is purely the user's call.
- Every SessionDeclaration implementation already had close() = undeclare();
  the interface now declares it, so a value typed as SessionDeclaration works
  with try-with-resources / use. The existing overrides stay.

Closes #518
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant